{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "635d8ebb",
   "metadata": {},
   "source": [
    "# LangGraph-Add-Query-Rewrite\n",
    "\n",
    "- Author: [Sunworl Kim](https://github.com/sunworl)\n",
    "- Design: [LeeYuChul](https://github.com/LeeYuChul)\n",
    "- Peer Review:\n",
    "- Proofread : [Chaeyoon Kim](https://github.com/chaeyoonyunakim)\n",
    "- This is a part of [LangChain Open Tutorial](https://github.com/LangChain-OpenTutorial/LangChain-OpenTutorial)\n",
    "\n",
    "[![Open in Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/LangChain-OpenTutorial/LangChain-OpenTutorial/blob/main/17-LangGraph/02-Structures/05-LangGraph-Add-Query-Rewrite.ipynb)[![Open in GitHub](https://img.shields.io/badge/Open%20in%20GitHub-181717?style=flat-square&logo=github&logoColor=white)](https://github.com/LangChain-OpenTutorial/LangChain-OpenTutorial/blob/main/17-LangGraph/02-Structures/05-LangGraph-Add-Query-Rewrite.ipynb)\n",
    "## Overview\n",
    "\n",
    "In this tutorial, we will cover the process of restructuring the original question by incorporating a **Query Rewrite** step. When a user's question is received, it enables more effective searching. While using Naive RAG as a foundation, this process aims for us to add supplementary mechanisms through query rewriting and web search. The procedure is as follows:\n",
    "\n",
    "1. Perform Naive RAG\n",
    "2. Conduct Groundedness Check for Retrieved Documents (Groundedness Check)\n",
    "3. Web Search\n",
    "4. (This Tutorial) Query Rewrite \n",
    "\n",
    "[Note]\n",
    "As this builds upon a previous tutorial, there may be overlapping content. For any missing explanations, please refer to the previous tutorial.\n",
    "\n",
    "![image.png](assets/05-langgraph-add-query-rewrite-01.png)\n",
    "\n",
    "\n",
    "\n",
    "### Table of Contents\n",
    "\n",
    "- [Overview](#overview)\n",
    "- [Environment Setup](#environment-setup)\n",
    "- [Creating a Basic PDF-based Retrieval Chain](#creating-a-basic-pdf-based-retrieval-chain)\n",
    "- [Defining GraphState](#defining-graphstate)\n",
    "- [Defining Nodes](#defining-nodes)\n",
    "- [Adding the Query Rewrite Node](#adding-the-query-rewrite-node)\n",
    "- [Defining Edges](#defining-edges)\n",
    "- [Running Graph](#running-graph)\n",
    "\n",
    "\n",
    "### References\n",
    "\n",
    "- [langgraph_agentic_rag](https://github.com/langchain-ai/langgraph/blob/main/examples/rag/langgraph_agentic_rag.ipynb)\n",
    "\n",
    "----"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c6c7aba4",
   "metadata": {},
   "source": [
    "## Environment Setup\n",
    "\n",
    "Set up the environment. You may refer to [Environment Setup](https://wikidocs.net/257836) for more details.\n",
    "\n",
    "**[Note]**\n",
    "- ```langchain-opentutorial``` is a package that provides a set of easy-to-use environment setup, useful functions and utilities for tutorials. \n",
    "\n",
    "- You can checkout the [```langchain-opentutorial```](https://github.com/LangChain-OpenTutorial/langchain-opentutorial-pypi) for more details."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 28,
   "id": "21943adb",
   "metadata": {},
   "outputs": [],
   "source": [
    "%%capture --no-stderr\n",
    "%pip install langchain-opentutorial"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 29,
   "id": "f25ec196",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Install required packages\n",
    "from langchain_opentutorial import package\n",
    "\n",
    "package.install(\n",
    "    [\n",
    "        \"langsmith\",\n",
    "        \"langchain\",\n",
    "        \"langchain_openai\",\n",
    "        \"langgraph\",\n",
    "        \"typing\",\n",
    "        \"langchain_core\",\n",
    "        \"langchain-opentutorial\"\n",
    "    ],\n",
    "    verbose=False,\n",
    "    upgrade=False,\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "9158d8d7",
   "metadata": {},
   "source": [
    "You can alternatively set API keys such as ```OPENAI_API_KEY``` in a ```.env``` file and load them.\n",
    "\n",
    "[Note] This is not necessary if you've already set the required API keys in previous steps."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 30,
   "id": "7f9065ea",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Environment variables have been set successfully.\n"
     ]
    }
   ],
   "source": [
    "from dotenv import load_dotenv\n",
    "from langchain_opentutorial import set_env\n",
    "\n",
    "# Attempt to load environment variables from a .env file; if unsuccessful, set them manually.\n",
    "if not load_dotenv():\n",
    "    set_env(\n",
    "        {\n",
    "            \"OPENAI_API_KEY\": \"\",\n",
    "            \"LANGCHAIN_API_KEY\": \"\",\n",
    "            \"LANGCHAIN_TRACING_V2\": \"true\",\n",
    "            \"LANGCHAIN_ENDPOINT\": \"https://api.smith.langchain.com\",\n",
    "            \"LANGCHAIN_PROJECT\": \"05-LangGraph-Add-Query-Rewrite\"  \n",
    "         }\n",
    "    )"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2b2fc536",
   "metadata": {},
   "source": [
    "## Creating a Basic PDF-based Retrieval Chain\n",
    "\n",
    "Here, we create a Retrieval Chain based on PDF documents. This is the simplest structure of a Retrieval Chain.\n",
    "\n",
    "However, in LangGraph, the Retriever and Chain are created separately. This allows for detailed processing at each node.\n",
    "\n",
    "**[Note]**\n",
    "\n",
    "Detailed explanations for this step can be found in the previous tutorial.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 31,
   "id": "1b78d33f",
   "metadata": {},
   "outputs": [],
   "source": [
    "from rag.pdf import PDFRetrievalChain\n",
    "\n",
    "# Load the PDF document\n",
    "pdf = PDFRetrievalChain([\"data/A European Approach to Artificial Intelligence - A Policy Perspective.pdf\"]).create_chain()\n",
    "\n",
    "# Create retriever and chain\n",
    "pdf_retriever = pdf.retriever\n",
    "pdf_chain = pdf.chain"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c9e4d831",
   "metadata": {},
   "source": [
    "## Defining ```GraphState```\n",
    "\n",
    "The ```GraphState``` object defines the shared state between nodes in the Langgraph.\n",
    "\n",
    "Generally, using a ```TypedDict``` is to ensure type safety and maintain a clear structure for the graph's state management.\n",
    "\n",
    "In this case, we'll add a field for relevance check results to the state.\n",
    "\n",
    "**[Note]**\n",
    "\n",
    "Here, we define ```question``` as a list to accomodate the rewritten queries that are additionally stored."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 32,
   "id": "0874c14b",
   "metadata": {},
   "outputs": [],
   "source": [
    "from typing import Annotated, TypedDict\n",
    "from langgraph.graph.message import add_messages\n",
    "\n",
    "\n",
    "# Define GraphState\n",
    "class GraphState(TypedDict):\n",
    "    question: Annotated[list[str], add_messages]  # Questions (accumulating list)\n",
    "    context: Annotated[str, \"Context\"]  # Search results from documents\n",
    "    answer: Annotated[str, \"Answer\"]  # Response\n",
    "    messages: Annotated[list, add_messages]  # Messages (accumulating list)\n",
    "    relevance: Annotated[str, \"Relevance\"]  # Relevance score"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d7c108df",
   "metadata": {},
   "source": [
    "## Defining Nodes\n",
    "\n",
    "**Nodes** represent individual processing steps. They are typically implemented as Python functions. Both input and output are ```state``` values.\n",
    "\n",
    "**[Note]**\n",
    "\n",
    "Each Node takes a ```state``` as input, performs its designated logic, and returns an updated ```state```."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 33,
   "id": "2f56baf8",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_openai import ChatOpenAI\n",
    "from langchain_opentutorial.evaluator import GroundednessChecker\n",
    "from langchain_opentutorial.messages import messages_to_history\n",
    "from langchain_opentutorial.tools.tavily import TavilySearch\n",
    "from rag.utils import format_docs\n",
    "\n",
    "\n",
    "# Document retrieval node\n",
    "def retrieve_document(state: GraphState) -> GraphState:\n",
    "    # Get the question from the state\n",
    "    latest_question = state[\"question\"][-1].content\n",
    "\n",
    "    # Search the documents to find relevant ones\n",
    "    retrieved_docs = pdf_retriever.invoke(latest_question)\n",
    "\n",
    "    # Format the retrieved documents (to be used as prompt input)\n",
    "    retrieved_docs = format_docs(retrieved_docs)\n",
    "\n",
    "    # Store the retrieved documents in the context key\n",
    "    return GraphState(context=retrieved_docs)\n",
    "\n",
    "\n",
    "# Answer generation node\n",
    "def llm_answer(state: GraphState) -> GraphState:\n",
    "    # Get the question from the state\n",
    "    latest_question = state[\"question\"][-1].content\n",
    "\n",
    "    # Get the retrieved documents from the state\n",
    "    context = state[\"context\"]\n",
    "\n",
    "    # Call the chain to generate an answer\n",
    "    response = pdf_chain.invoke(\n",
    "        {\n",
    "            \"question\": latest_question,\n",
    "            \"context\": context,\n",
    "            \"chat_history\": messages_to_history(state[\"messages\"]),\n",
    "        }\n",
    "    )\n",
    "    # Store the generated answer and (user question, answer) messages in the state\n",
    "    return {\n",
    "        \"answer\": response,\n",
    "        \"messages\": [(\"user\", latest_question), (\"assistant\", response)],\n",
    "    }\n",
    "\n",
    "\n",
    "# Relevance check node\n",
    "def relevance_check(state: GraphState) -> GraphState:\n",
    "    # Create a relevance evaluator\n",
    "    question_answer_relevant = GroundednessChecker(\n",
    "        llm=ChatOpenAI(model=\"gpt-4o-mini\", temperature=0), target=\"question-retrieval\"\n",
    "    ).create()\n",
    "\n",
    "    # Execute relevance check\n",
    "    response = question_answer_relevant.invoke(\n",
    "        {\"question\": state[\"question\"][-1].content, \"context\": state[\"context\"]}\n",
    "    )\n",
    "\n",
    "    # Return the groundedness score\n",
    "    # Note: The groundedness checker here can be modified using your own prompt. Try creating and using your own Groundedness Check!\n",
    "    return {\"relevance\": response.score}\n",
    "\n",
    "\n",
    "# Function to check relevance (router)\n",
    "def is_relevant(state: GraphState) -> GraphState:\n",
    "    if state[\"relevance\"] == \"yes\":\n",
    "        return \"relevant\"\n",
    "    else:\n",
    "        return \"not relevant\"\n",
    "\n",
    "\n",
    "# Web Search node\n",
    "def web_search(state: GraphState) -> GraphState:\n",
    "    # Create search tool\n",
    "    tavily_tool = TavilySearch()\n",
    "\n",
    "    search_query = state[\"question\"]\n",
    "\n",
    "    # Search example using various parameters\n",
    "    search_result = tavily_tool.search(\n",
    "        query=search_query,  # Search query\n",
    "        topic=\"general\",  # General topic\n",
    "        max_results=6,  # Maximum search results\n",
    "        format_output=True,  # results format\n",
    "    )\n",
    "\n",
    "    return {\"context\": search_result}"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "25a0901c",
   "metadata": {},
   "source": [
    "## Adding the Query Rewrite Node\n",
    "\n",
    "The **Query Rewrite Node** rewrites the original questions, using a prompt designed for query reformulation."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 34,
   "id": "a974ebbe",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_core.prompts import PromptTemplate\n",
    "from langchain_core.output_parsers import StrOutputParser\n",
    "\n",
    "# Define the prompt template for query rewriting\n",
    "re_write_prompt = PromptTemplate(\n",
    "    template=\"\"\"Reformulate the given question to enhance its effectiveness for vectorstore retrieval.\n",
    "\n",
    "- Analyze the initial question to identify areas for improvement such as specificity, clarity, and relevance.\n",
    "- Consider the context and potential keywords that would optimize retrieval.\n",
    "- Maintain the intent of the original question while enhancing its structure and vocabulary.\n",
    "\n",
    "# Steps\n",
    "\n",
    "1. **Understand the Original Question**: Identify the core intent and any keywords.\n",
    "2. **Enhance Clarity**: Simplify language and ensure the question is direct and to the point.\n",
    "3. **Optimize for Retrieval**: Add or rearrange keywords for better alignment with vectorstore indexing.\n",
    "4. **Review**: Ensure the improved question accurately reflects the original intent and is free of ambiguity.\n",
    "\n",
    "# Output Format\n",
    "\n",
    "- Provide a single, improved question.\n",
    "- Do not include any introductory or explanatory text; only the reformulated question.\n",
    "\n",
    "# Examples\n",
    "\n",
    "**Input**: \n",
    "\"What are the benefits of using renewable energy sources over fossil fuels?\"\n",
    "\n",
    "**Output**: \n",
    "\"How do renewable energy sources compare to fossil fuels in terms of benefits?\"\n",
    "\n",
    "**Input**: \n",
    "\"How does climate change impact polar bear populations?\"\n",
    "\n",
    "**Output**: \n",
    "\"What effects does climate change have on polar bear populations?\"\n",
    "\n",
    "# Notes\n",
    "\n",
    "- Ensure the improved question is concise and contextually relevant.\n",
    "- Avoid altering the fundamental intent or meaning of the original question.\n",
    "\n",
    "\n",
    "[REMEMBER] Re-written question should be in the same language as the original question.\n",
    "\n",
    "# Here is the original question that needs to be rewritten:\n",
    "{question}\n",
    "\"\"\",\n",
    "    input_variables=[\"generation\", \"question\"],\n",
    ")\n",
    "\n",
    "question_rewriter = (\n",
    "    re_write_prompt | ChatOpenAI(model=\"gpt-4o-mini\", temperature=0) | StrOutputParser()\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c45ed7be",
   "metadata": {},
   "source": [
    "This node utilizes a ```question_rewriter``` function to rewrite the question."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 35,
   "id": "1c8e5a81",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "'What are the current applications of AI in healthcare and their limitations?'"
      ]
     },
     "execution_count": 35,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# Query Rewriting\n",
    "question = \"Where has the application of AI in healthcare been confined to so far?\"\n",
    "\n",
    "question_rewriter.invoke({\"question\": question})"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 36,
   "id": "e2c5b430",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Query Rewrite Node\n",
    "def query_rewrite(state: GraphState) -> GraphState:\n",
    "    latest_question = state[\"question\"][-1].content\n",
    "    question_rewritten = question_rewriter.invoke({\"question\": latest_question})\n",
    "    return {\"question\": question_rewritten}"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5e07e356",
   "metadata": {},
   "source": [
    "## Defining Edges\n",
    "\n",
    "**Edges** determine the next **Node** to execute based on the current ```state```.\n",
    "\n",
    "There are different types such as regular edges and conditional edges."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 37,
   "id": "004c1d20",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langgraph.graph import END, StateGraph\n",
    "from langgraph.checkpoint.memory import MemorySaver\n",
    "\n",
    "# Define graph\n",
    "workflow = StateGraph(GraphState)\n",
    "\n",
    "# Add nodes\n",
    "workflow.add_node(\"retrieve\", retrieve_document)\n",
    "workflow.add_node(\"relevance_check\", relevance_check)\n",
    "workflow.add_node(\"llm_answer\", llm_answer)\n",
    "workflow.add_node(\"web_search\", web_search)\n",
    "\n",
    "# Add Query Rewrite node\n",
    "workflow.add_node(\"query_rewrite\", query_rewrite)\n",
    "\n",
    "# Add edges\n",
    "workflow.add_edge(\"query_rewrite\", \"retrieve\")  # Query rewrite -> Retrieval\n",
    "workflow.add_edge(\"retrieve\", \"relevance_check\")  # Retrieval -> Relevance check\n",
    "\n",
    "# Add conditional edges\n",
    "workflow.add_conditional_edges(\n",
    "    \"relevance_check\",  # Passes the output from relevance check node to is_relevant function\n",
    "    is_relevant,\n",
    "    {\n",
    "        \"relevant\": \"llm_answer\",  # If relevant, generate answer\n",
    "        \"not relevant\": \"web_search\",  # If not relevant, perform web search\n",
    "    },\n",
    ")\n",
    "\n",
    "workflow.add_edge(\"web_search\", \"llm_answer\")  # Web search -> Answer\n",
    "workflow.add_edge(\"llm_answer\", END)  # Answer -> End\n",
    "\n",
    "# Set graph entry point\n",
    "workflow.set_entry_point(\"query_rewrite\")\n",
    "\n",
    "# Set checkpointer\n",
    "memory = MemorySaver()\n",
    "\n",
    "# Compile graph\n",
    "app = workflow.compile(checkpointer=memory)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f1bee360",
   "metadata": {},
   "source": [
    "Let's visualize the compiled graph structure."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 38,
   "id": "aeac1b1b",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "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",
      "text/plain": [
       "<IPython.core.display.Image object>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "from langchain_opentutorial.graphs import visualize_graph\n",
    "\n",
    "visualize_graph(app)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5fc43b99",
   "metadata": {},
   "source": [
    "## Running Graph\n",
    "\n",
    "- ```config``` parameter: Passes configuration settings required for running the graph.\n",
    "\n",
    "- ```recursion_limit```: Sets a limit on the number of recursions when running the graph.\n",
    "\n",
    "- ```inputs```: Passes the input information required for running the graph.\n",
    "\n",
    "\n",
    "If the ```relevance_check``` of the initial search results fails, it performs web search and provides web search results."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 39,
   "id": "d1221d80",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "==================================================\n",
      "🔄 Node: \u001b[1;36mquery_rewrite\u001b[0m 🔄\n",
      "- - - - - - - - - - - - - - - - - - - - - - - - - \n",
      "\u001b[1;32mquestion\u001b[0m:\n",
      "What are the current applications of AI in healthcare?\n",
      "==================================================\n",
      "\n",
      "==================================================\n",
      "🔄 Node: \u001b[1;36mretrieve\u001b[0m 🔄\n",
      "- - - - - - - - - - - - - - - - - - - - - - - - - \n",
      "\u001b[1;32mcontext\u001b[0m:\n",
      "<document><content>activities. So far, however, AI applications in healthcare have been potential. Specific healthcare training should be provided to data\n",
      "confined to administrative tasks (i.e., Natural Language Processing scientists working in hospitals so that they can better understand</content><source>data/A European Approach to Artificial Intelligence - A Policy Perspective.pdf</source><page>15</page></document>\n",
      "<document><content>are great, as more use of AI in research and development could\n",
      "Healthcare is arguably the sector where AI could make the lead to a more personalised healthcare based on patients’ data.</content><source>data/A European Approach to Artificial Intelligence - A Policy Perspective.pdf</source><page>15</page></document>\n",
      "<document><content>same. The Covid-19 crisis has shown how strained our National\n",
      "Healthcare Systems are, and AI solutions could help meet the cur- AI in the healthcare faces organisational and skill challenges. One</content><source>data/A European Approach to Artificial Intelligence - A Policy Perspective.pdf</source><page>15</page></document>\n",
      "<document><content>advanced robots, autonomous cars, drones or Internet of Things place, a recent EIT Health Report envisages more in healthcare in\n",
      "applications)”. Broad AI definitions cover several technologies, in- the near future, such as remote monitoring, AI-powered alerting</content><source>data/A European Approach to Artificial Intelligence - A Policy Perspective.pdf</source><page>4</page></document>\n",
      "<document><content>EIT Health and McKinsey & Company, (2020), Transforming healthcare with AI. Impact Scherer, M. (2016). Regulating Artificial Intelligence Systems: Risks, Challenges, Compe-</content><source>data/A European Approach to Artificial Intelligence - A Policy Perspective.pdf</source><page>22</page></document>\n",
      "<document><content>intermediate / professional users (i.e., healthcare professionals). the safety of employees. The key application of AI is certainly in\n",
      "This is a matter of privacy and personal data protection, of building predictive maintenance. Yet, the more radical transformation of</content><source>data/A European Approach to Artificial Intelligence - A Policy Perspective.pdf</source><page>11</page></document>\n",
      "<document><content>very sensitive. An extensive use to feed AI tools can the use of patient’s data in the hospitals that deploy\n",
      "Health data raise many concerns. Data ownership is also an issue AI-powered applications. The patients should be aware</content><source>data/A European Approach to Artificial Intelligence - A Policy Perspective.pdf</source><page>16</page></document>\n",
      "<document><content>Remote sible, as AI solutions can increasingly divert patients ning healthcare professionals, starting from the simple\n",
      "healthcare to appropriate solutions for their specific symptoms tasks and diagnostic appointments.\n",
      "and underlying conditions.\n",
      "16</content><source>data/A European Approach to Artificial Intelligence - A Policy Perspective.pdf</source><page>16</page></document>\n",
      "<document><content>greatest impact in addressing societal challenges. Given rising de- A second challenge is that of finding a common language and un-\n",
      "mands and costs, AI could help doing more and better with the derstanding between data experts and healthcare professionals.</content><source>data/A European Approach to Artificial Intelligence - A Policy Perspective.pdf</source><page>15</page></document>\n",
      "<document><content>to extract information from clinical notes or predictive scheduling healthcare practitioners needs. In addition, at the regulatory le-\n",
      "of the visits) and diagnostic (machine and deep learning applied to vel it is important that new AI regulation is harmonised with other</content><source>data/A European Approach to Artificial Intelligence - A Policy Perspective.pdf</source><page>15</page></document>\n",
      "==================================================\n",
      "\n",
      "==================================================\n",
      "🔄 Node: \u001b[1;36mrelevance_check\u001b[0m 🔄\n",
      "- - - - - - - - - - - - - - - - - - - - - - - - - \n",
      "\u001b[1;32mrelevance\u001b[0m:\n",
      "yes\n",
      "==================================================\n",
      "\n",
      "==================================================\n",
      "🔄 Node: \u001b[1;36mllm_answer\u001b[0m 🔄\n",
      "- - - - - - - - - - - - - - - - - - - - - - - - - \n",
      "\u001b[1;32manswer\u001b[0m:\n",
      "Current applications of AI in healthcare include remote monitoring, AI-powered alerting, predictive maintenance, and the use of machine and deep learning for extracting information from clinical notes and predictive scheduling of visits. These applications aim to enhance personalized healthcare based on patient data and improve the efficiency of healthcare systems.\n",
      "\n",
      "**Source**\n",
      "- data/A European Approach to Artificial Intelligence - A Policy Perspective.pdf (page 4, 15, 16)\n",
      "('user', 'What are the current applications of AI in healthcare?')\n",
      "('assistant', 'Current applications of AI in healthcare include remote monitoring, AI-powered alerting, predictive maintenance, and the use of machine and deep learning for extracting information from clinical notes and predictive scheduling of visits. These applications aim to enhance personalized healthcare based on patient data and improve the efficiency of healthcare systems.\\n\\n**Source**\\n- data/A European Approach to Artificial Intelligence - A Policy Perspective.pdf (page 4, 15, 16)')\n",
      "==================================================\n",
      "\n",
      "--- OUTPUTS ---\n",
      "\n",
      "Question: [HumanMessage(content='Where has the application of AI in healthcare been confined to so far?', additional_kwargs={}, response_metadata={}, id='1810913a-a2e8-4a5c-a5e2-4385db8d7ec2'), HumanMessage(content='What are the current applications of AI in healthcare?', additional_kwargs={}, response_metadata={}, id='26f3404e-f469-4926-bfd3-f82b666678c6')]\n",
      "Answer:\n",
      " Current applications of AI in healthcare include remote monitoring, AI-powered alerting, predictive maintenance, and the use of machine and deep learning for extracting information from clinical notes and predictive scheduling of visits. These applications aim to enhance personalized healthcare based on patient data and improve the efficiency of healthcare systems.\n",
      "\n",
      "**Source**\n",
      "- data/A European Approach to Artificial Intelligence - A Policy Perspective.pdf (page 4, 15, 16)\n",
      "Relevance: yes\n"
     ]
    }
   ],
   "source": [
    "from langchain_core.runnables import RunnableConfig\n",
    "from langchain_opentutorial.messages import stream_graph, invoke_graph, random_uuid\n",
    "from langgraph.errors import GraphRecursionError\n",
    "\n",
    "# Configure settings (max recursion count, thread_id)\n",
    "config = RunnableConfig(recursion_limit=10, configurable={\"thread_id\": random_uuid()})\n",
    "\n",
    "# Input question\n",
    "inputs = GraphState(question=\"Where has the application of AI in healthcare been confined to so far?\")\n",
    "\n",
    "# Execute the graph\n",
    "try:\n",
    "    invoke_graph(app, inputs, config)\n",
    "    outputs = app.get_state(config)\n",
    "\n",
    "    # Access the values dictionary within StateSnapshot\n",
    "    output_values = outputs.values\n",
    "\n",
    "    # Display the results\n",
    "    print(\"\\n--- OUTPUTS ---\\n\")\n",
    "    print(\"Question:\", output_values[\"question\"])\n",
    "    print(\"Answer:\\n\", output_values[\"answer\"])\n",
    "    print(\"Relevance:\", output_values[\"relevance\"])\n",
    "except GraphRecursionError as recursion_error:\n",
    "    print(f\"GraphRecursionError: {recursion_error}\")\n",
    "except Exception as e:\n",
    "    print(f\"Unexpected Error: {e}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "927cac10",
   "metadata": {},
   "source": [
    "The evaluator summarized above follows this."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 40,
   "id": "2fa5e0d7",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "==================================================\n",
      "🔄 Node: \u001b[1;36mquery_rewrite\u001b[0m 🔄\n",
      "- - - - - - - - - - - - - - - - - - - - - - - - - \n",
      "What are the current applications of AI in healthcare and their limitations?\n",
      "==================================================\n",
      "🔄 Node: \u001b[1;36mllm_answer\u001b[0m 🔄\n",
      "- - - - - - - - - - - - - - - - - - - - - - - - - \n",
      "Current applications of AI in healthcare include remote monitoring, AI-powered alerting, predictive maintenance, and personalized healthcare based on patient data. However, limitations include organizational and skill challenges, concerns over data ownership and privacy, and the need for better communication between data experts and healthcare professionals.\n",
      "\n",
      "**Source**\n",
      "- data/A European Approach to Artificial Intelligence - A Policy Perspective.pdf (pages 4, 15, 16)"
     ]
    }
   ],
   "source": [
    "# Running graph\n",
    "stream_graph(app, inputs, config, [\"query_rewrite\", \"llm_answer\"])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 41,
   "id": "ef397b71",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Original Question: Where has the application of AI in healthcare been confined to so far?\n",
      "Rewritten Question: What are the current applications of AI in healthcare and their limitations?\n",
      "============================================================\n",
      "Answer:\n",
      "Current applications of AI in healthcare include remote monitoring, AI-powered alerting, predictive maintenance, and personalized healthcare based on patient data. However, limitations include organizational and skill challenges, concerns over data ownership and privacy, and the need for better communication between data experts and healthcare professionals.\n",
      "\n",
      "**Source**\n",
      "- data/A European Approach to Artificial Intelligence - A Policy Perspective.pdf (pages 4, 15, 16)\n"
     ]
    }
   ],
   "source": [
    "# Check final outputs\n",
    "outputs = app.get_state(config).values\n",
    "\n",
    "print(f'Original Question: {outputs[\"question\"][0].content}')\n",
    "print(f'Rewritten Question: {outputs[\"question\"][-1].content}')\n",
    "print(\"===\" * 20)\n",
    "print(f'Answer:\\n{outputs[\"answer\"]}')"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "langchain-opentutorial-PEU2YJdv-py3.11",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.11.9"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
